/*
 * Copyright 2002-2017 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.aop.aspectj.annotation;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.lang.reflect.Method;

import org.aopalliance.aop.Advice;
import org.aspectj.lang.reflect.PerClauseKind;

import org.springframework.aop.Pointcut;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.aspectj.AspectJPrecedenceInformation;
import org.springframework.aop.aspectj.InstantiationModelAwarePointcutAdvisor;
import org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactory.AspectJAnnotation;
import org.springframework.aop.support.DynamicMethodMatcherPointcut;
import org.springframework.aop.support.Pointcuts;
import org.springframework.lang.Nullable;

/**
 * Internal implementation of AspectJPointcutAdvisor.
 * Note that there will be one instance of this advisor for each target method.
 *
 * @author Rod Johnson
 * @author Juergen Hoeller
 * @since 2.0
 */
@SuppressWarnings("serial")
class InstantiationModelAwarePointcutAdvisorImpl
        implements InstantiationModelAwarePointcutAdvisor, AspectJPrecedenceInformation, Serializable {

    private static final Advice EMPTY_ADVICE = new Advice() {
    };


    private final AspectJExpressionPointcut declaredPointcut;

    private final Class<?> declaringClass;

    private final String methodName;

    private final Class<?>[] parameterTypes;
    private final AspectJAdvisorFactory aspectJAdvisorFactory;
    private final MetadataAwareAspectInstanceFactory aspectInstanceFactory;
    private final int declarationOrder;
    private final String aspectName;
    private final Pointcut pointcut;
    private final boolean lazy;
    private transient Method aspectJAdviceMethod;
    @Nullable
    private Advice instantiatedAdvice;

    @Nullable
    private Boolean isBeforeAdvice;

    @Nullable
    private Boolean isAfterAdvice;


    public InstantiationModelAwarePointcutAdvisorImpl(AspectJExpressionPointcut declaredPointcut,
            Method aspectJAdviceMethod, AspectJAdvisorFactory aspectJAdvisorFactory,
            MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrder, String aspectName) {

        this.declaredPointcut = declaredPointcut;
        this.declaringClass = aspectJAdviceMethod.getDeclaringClass();
        this.methodName = aspectJAdviceMethod.getName();
        this.parameterTypes = aspectJAdviceMethod.getParameterTypes();
        this.aspectJAdviceMethod = aspectJAdviceMethod;
        this.aspectJAdvisorFactory = aspectJAdvisorFactory;
        this.aspectInstanceFactory = aspectInstanceFactory;
        this.declarationOrder = declarationOrder;
        this.aspectName = aspectName;

        if (aspectInstanceFactory.getAspectMetadata().isLazilyInstantiated()) {
            // Static part of the pointcut is a lazy type.
            Pointcut preInstantiationPointcut = Pointcuts.union(
                    aspectInstanceFactory.getAspectMetadata().getPerClausePointcut(), this.declaredPointcut);

            // Make it dynamic: must mutate from pre-instantiation to post-instantiation state.
            // If it's not a dynamic pointcut, it may be optimized out
            // by the Spring AOP infrastructure after the first evaluation.
            this.pointcut = new PerTargetInstantiationModelPointcut(
                    this.declaredPointcut, preInstantiationPointcut, aspectInstanceFactory);
            this.lazy = true;
        } else {
            // A singleton aspect.
            this.pointcut = this.declaredPointcut;
            this.lazy = false;
            this.instantiatedAdvice = instantiateAdvice(this.declaredPointcut);
        }
    }


    /**
     * The pointcut for Spring AOP to use. Actual behaviour of the pointcut will change
     * depending on the state of the advice.
     */
    @Override
    public Pointcut getPointcut() {
        return this.pointcut;
    }

    /**
     * This is only of interest for Spring AOP: AspectJ instantiation semantics
     * are much richer. In AspectJ terminology, all a return of {@code true}
     * means here is that the aspect is not a SINGLETON.
     */
    @Override
    public boolean isPerInstance() {
        return (getAspectMetadata().getAjType().getPerClause().getKind() != PerClauseKind.SINGLETON);
    }

    /**
     * Return the AspectJ AspectMetadata for this advisor.
     */
    public AspectMetadata getAspectMetadata() {
        return this.aspectInstanceFactory.getAspectMetadata();
    }

    /**
     * Lazily instantiate advice if necessary.
     */
    @Override
    public synchronized Advice getAdvice() {
        if (this.instantiatedAdvice == null) {
            this.instantiatedAdvice = instantiateAdvice(this.declaredPointcut);
        }
        return this.instantiatedAdvice;
    }

    @Override
    public boolean isLazy() {
        return this.lazy;
    }

    @Override
    public synchronized boolean isAdviceInstantiated() {
        return (this.instantiatedAdvice != null);
    }


    private Advice instantiateAdvice(AspectJExpressionPointcut pointcut) {
        Advice advice = this.aspectJAdvisorFactory.getAdvice(this.aspectJAdviceMethod, pointcut,
                this.aspectInstanceFactory, this.declarationOrder, this.aspectName);
        return (advice != null ? advice : EMPTY_ADVICE);
    }

    public MetadataAwareAspectInstanceFactory getAspectInstanceFactory() {
        return this.aspectInstanceFactory;
    }

    public AspectJExpressionPointcut getDeclaredPointcut() {
        return this.declaredPointcut;
    }

    @Override
    public int getOrder() {
        return this.aspectInstanceFactory.getOrder();
    }

    @Override
    public String getAspectName() {
        return this.aspectName;
    }

    @Override
    public int getDeclarationOrder() {
        return this.declarationOrder;
    }

    @Override
    public boolean isBeforeAdvice() {
        if (this.isBeforeAdvice == null) {
            determineAdviceType();
        }
        return this.isBeforeAdvice;
    }

    @Override
    public boolean isAfterAdvice() {
        if (this.isAfterAdvice == null) {
            determineAdviceType();
        }
        return this.isAfterAdvice;
    }

    /**
     * Duplicates some logic from getAdvice, but importantly does not force
     * creation of the advice.
     */
    private void determineAdviceType() {
        AspectJAnnotation<?> aspectJAnnotation =
                AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(this.aspectJAdviceMethod);
        if (aspectJAnnotation == null) {
            this.isBeforeAdvice = false;
            this.isAfterAdvice = false;
        } else {
            switch (aspectJAnnotation.getAnnotationType()) {
                case AtAfter:
                case AtAfterReturning:
                case AtAfterThrowing:
                    this.isAfterAdvice = true;
                    this.isBeforeAdvice = false;
                    break;
                case AtAround:
                case AtPointcut:
                    this.isAfterAdvice = false;
                    this.isBeforeAdvice = false;
                    break;
                case AtBefore:
                    this.isAfterAdvice = false;
                    this.isBeforeAdvice = true;
            }
        }
    }


    @Override
    public String toString() {
        return "InstantiationModelAwarePointcutAdvisor: expression [" + getDeclaredPointcut().getExpression() +
                "]; advice method [" + this.aspectJAdviceMethod + "]; perClauseKind=" +
                this.aspectInstanceFactory.getAspectMetadata().getAjType().getPerClause().getKind();

    }

    private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException {
        inputStream.defaultReadObject();
        try {
            this.aspectJAdviceMethod = this.declaringClass.getMethod(this.methodName, this.parameterTypes);
        } catch (NoSuchMethodException ex) {
            throw new IllegalStateException("Failed to find advice method on deserialization", ex);
        }
    }


    /**
     * Pointcut implementation that changes its behaviour when the advice is instantiated.
     * Note that this is a <i>dynamic</i> pointcut. Otherwise it might
     * be optimized out if it does not at first match statically.
     */
    private class PerTargetInstantiationModelPointcut extends DynamicMethodMatcherPointcut {

        private final AspectJExpressionPointcut declaredPointcut;

        private final Pointcut preInstantiationPointcut;

        @Nullable
        private LazySingletonAspectInstanceFactoryDecorator aspectInstanceFactory;

        private PerTargetInstantiationModelPointcut(AspectJExpressionPointcut declaredPointcut,
                Pointcut preInstantiationPointcut, MetadataAwareAspectInstanceFactory aspectInstanceFactory) {

            this.declaredPointcut = declaredPointcut;
            this.preInstantiationPointcut = preInstantiationPointcut;
            if (aspectInstanceFactory instanceof LazySingletonAspectInstanceFactoryDecorator) {
                this.aspectInstanceFactory = (LazySingletonAspectInstanceFactoryDecorator) aspectInstanceFactory;
            }
        }

        @Override
        public boolean matches(Method method, @Nullable Class<?> targetClass) {
            // We're either instantiated and matching on declared pointcut, or uninstantiated matching on either pointcut
            return (isAspectMaterialized() && this.declaredPointcut.matches(method, targetClass)) ||
                    this.preInstantiationPointcut.getMethodMatcher().matches(method, targetClass);
        }

        @Override
        public boolean matches(Method method, @Nullable Class<?> targetClass, Object... args) {
            // This can match only on declared pointcut.
            return (isAspectMaterialized() && this.declaredPointcut.matches(method, targetClass));
        }

        private boolean isAspectMaterialized() {
            return (this.aspectInstanceFactory == null || this.aspectInstanceFactory.isMaterialized());
        }
    }

}
